// ========================================================================
// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at 
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses. 
// ========================================================================

package org.eclipse.jetty.server.handler;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.http.PathMap;
import org.eclipse.jetty.server.AsyncContinuation;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HandlerContainer;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/* ------------------------------------------------------------ */
/**
 * ContextHandlerCollection. This {@link org.eclipse.jetty.server.handler.HandlerCollection} is creates a {@link org.eclipse.jetty.http.PathMap} to it's contained handlers based on the context path and virtual hosts of any contained {@link org.eclipse.jetty.server.handler.ContextHandler}s. The contexts do not need to be directly contained, only children of the contained handlers. Multiple contexts may have the same context path and they are called in order until one handles the request.
 * 
 * @org.apache.xbean.XBean element="contexts"
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public class ContextHandlerCollection extends HandlerCollection
{

	private static final Logger LOG = Log.getLogger(ContextHandlerCollection.class);

	private volatile PathMap _contextMap;
	private Class<? extends ContextHandler> _contextClass = ContextHandler.class;

	/* ------------------------------------------------------------ */
	public ContextHandlerCollection()
	{
		super(true);
	}

	/* ------------------------------------------------------------ */
	/**
	 * Remap the context paths.
	 */
	public void mapContexts()
	{
		PathMap contextMap = new PathMap();
		Handler[] branches = getHandlers();

		for (int b = 0; branches != null && b < branches.length; b++)
		{
			Handler[] handlers = null;

			if (branches[b] instanceof ContextHandler)
			{
				handlers = new Handler[] { branches[b] };
			}
			else if (branches[b] instanceof HandlerContainer)
			{
				handlers = ((HandlerContainer)branches[b]).getChildHandlersByClass(ContextHandler.class);
			}
			else
				continue;

			for (int i = 0; i < handlers.length; i++)
			{
				ContextHandler handler = (ContextHandler)handlers[i];

				String contextPath = handler.getContextPath();

				if (contextPath == null || contextPath.indexOf(',') >= 0 || contextPath.startsWith("*"))
					throw new IllegalArgumentException("Illegal context spec:" + contextPath);

				if (!contextPath.startsWith("/"))
					contextPath = '/' + contextPath;

				if (contextPath.length() > 1)
				{
					if (contextPath.endsWith("/"))
						contextPath += "*";
					else if (!contextPath.endsWith("/*"))
						contextPath += "/*";
				}

				Object contexts = contextMap.get(contextPath);
				String[] vhosts = handler.getVirtualHosts();

				if (vhosts != null && vhosts.length > 0)
				{
					Map hosts;

					if (contexts instanceof Map)
						hosts = (Map)contexts;
					else
					{
						hosts = new HashMap();
						hosts.put("*", contexts);
						contextMap.put(contextPath, hosts);
					}

					for (int j = 0; j < vhosts.length; j++)
					{
						String vhost = vhosts[j];
						contexts = hosts.get(vhost);
						contexts = LazyList.add(contexts, branches[b]);
						hosts.put(vhost, contexts);
					}
				}
				else if (contexts instanceof Map)
				{
					Map hosts = (Map)contexts;
					contexts = hosts.get("*");
					contexts = LazyList.add(contexts, branches[b]);
					hosts.put("*", contexts);
				}
				else
				{
					contexts = LazyList.add(contexts, branches[b]);
					contextMap.put(contextPath, contexts);
				}
			}
		}
		_contextMap = contextMap;

	}

	/* ------------------------------------------------------------ */
	/* 
	 * @see org.eclipse.jetty.server.server.handler.HandlerCollection#setHandlers(org.eclipse.jetty.server.server.Handler[])
	 */
	@Override
	public void setHandlers(Handler[] handlers)
	{
		_contextMap = null;
		super.setHandlers(handlers);
		if (isStarted())
			mapContexts();
	}

	/* ------------------------------------------------------------ */
	@Override
	protected void doStart() throws Exception
	{
		mapContexts();
		super.doStart();
	}

	/* ------------------------------------------------------------ */
	/* 
	 * @see org.eclipse.jetty.server.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
	 */
	@Override
	public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
	{
		Handler[] handlers = getHandlers();
		if (handlers == null || handlers.length == 0)
			return;

		AsyncContinuation async = baseRequest.getAsyncContinuation();
		if (async.isAsync())
		{
			ContextHandler context = async.getContextHandler();
			if (context != null)
			{
				context.handle(target, baseRequest, request, response);
				return;
			}
		}

		// data structure which maps a request to a context; first-best match wins
		// { context path => 
		//     { virtual host => context } 
		// }
		PathMap map = _contextMap;
		if (map != null && target != null && target.startsWith("/"))
		{
			// first, get all contexts matched by context path
			Object contexts = map.getLazyMatches(target);

			for (int i = 0; i < LazyList.size(contexts); i++)
			{
				// then, match against the virtualhost of each context
				Map.Entry entry = (Map.Entry)LazyList.get(contexts, i);
				Object list = entry.getValue();

				if (list instanceof Map)
				{
					Map hosts = (Map)list;
					String host = normalizeHostname(request.getServerName());

					// explicitly-defined virtual hosts, most specific
					list = hosts.get(host);
					for (int j = 0; j < LazyList.size(list); j++)
					{
						Handler handler = (Handler)LazyList.get(list, j);
						handler.handle(target, baseRequest, request, response);
						if (baseRequest.isHandled())
							return;
					}

					// wildcard for one level of names 
					list = hosts.get("*." + host.substring(host.indexOf(".") + 1));
					for (int j = 0; j < LazyList.size(list); j++)
					{
						Handler handler = (Handler)LazyList.get(list, j);
						handler.handle(target, baseRequest, request, response);
						if (baseRequest.isHandled())
							return;
					}

					// no virtualhosts defined for the context, least specific
					// will handle any request that does not match to a specific virtual host above
					list = hosts.get("*");
					for (int j = 0; j < LazyList.size(list); j++)
					{
						Handler handler = (Handler)LazyList.get(list, j);
						handler.handle(target, baseRequest, request, response);
						if (baseRequest.isHandled())
							return;
					}
				}
				else
				{
					for (int j = 0; j < LazyList.size(list); j++)
					{
						Handler handler = (Handler)LazyList.get(list, j);
						handler.handle(target, baseRequest, request, response);
						if (baseRequest.isHandled())
							return;
					}
				}
			}
		}
		else
		{
			// This may not work in all circumstances... but then I think it should never be called
			for (int i = 0; i < handlers.length; i++)
			{
				handlers[i].handle(target, baseRequest, request, response);
				if (baseRequest.isHandled())
					return;
			}
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * Add a context handler.
	 * 
	 * @param contextPath The context path to add
	 * @return the ContextHandler just added
	 */
	public ContextHandler addContext(String contextPath, String resourceBase)
	{
		try
		{
			ContextHandler context = _contextClass.newInstance();
			context.setContextPath(contextPath);
			context.setResourceBase(resourceBase);
			addHandler(context);
			return context;
		} catch (Exception e)
		{
			LOG.debug(e);
			throw new Error(e);
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * @return The class to use to add new Contexts
	 */
	public Class getContextClass()
	{
		return _contextClass;
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param contextClass The class to use to add new Contexts
	 */
	public void setContextClass(Class contextClass)
	{
		if (contextClass == null || !(ContextHandler.class.isAssignableFrom(contextClass)))
			throw new IllegalArgumentException();
		_contextClass = contextClass;
	}

	/* ------------------------------------------------------------ */
	private String normalizeHostname(String host)
	{
		if (host == null)
			return null;

		if (host.endsWith("."))
			return host.substring(0, host.length() - 1);

		return host;
	}

}
